﻿using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using DeepEarth.Core;
using System.Windows;
using DeepEarth.Map.Core.Tiling;

#if !WINDOWS_PHONE
using System.Reactive.Linq;
#else
using Microsoft.Phone.Reactive;
#endif

namespace DeepEarth.Map.Core
{
	/// <summary>
	/// Describes a view for the DeepEarth map control.
	/// The map's view can be updated by editing the CurrentView object, or by setting it to a new MapView.
	/// </summary>

	public class MapView : INotifyPropertyChanged
	{
		public const string CENTER = "Center";
		public const string VIEWPORT_WIDTH = "ViewportWidth";
		public const string ZOOM_LEVEL = "ZoomLevel";
		public const string LAYERS = "Layers";
		public const string TILE_SOURCE_PROVIDER = "TileSourceProvider";
		public const string PROVIDER_TILESOURCES = "Provider.TileSources";

		public MapView(BaseTileSourceProvider provider)
		{
			if (provider == null) throw new ArgumentNullException("mode");

			TileSourceProvider = provider;
			Initialise();
		}

		// For XAML and Cloning
		public MapView()
		{
			Initialise();
		}

		/// <summary>
		///  Common constructor code
		/// </summary>
		void Initialise()
		{
			Center = new Location { Longitude = 0.0, Latitude = 0.0 };
			ViewportWidth = 1.0;

			SecondaryTileSourceProviders = new ObservableCollection<BaseTileSourceProvider>();

#if !WINDOWS_PHONE
			var locationChanged = Observable.FromEventPattern<PropertyChangedEventArgs>(this, "PropertyChanged")
				.Where(a => a.EventArgs.PropertyName == CENTER
				|| a.EventArgs.PropertyName == VIEWPORT_WIDTH);
#else
			var self = this;
			var locationChanged = Observable.FromEvent<PropertyChangedEventArgs>(this, "PropertyChanged")
				.Where(e => e.EventArgs.PropertyName == CENTER || e.EventArgs.PropertyName == VIEWPORT_WIDTH);
#endif

			//Raise the LocationChanged event.
			locationChanged
				.Throttle(TimeSpan.FromSeconds(1))
				.Subscribe(e => OnLocationChanged());
		}


		/// <summary>
		/// Fires One second after the location has changed
		/// </summary>
		public event EventHandler LocationChanged;
		void OnLocationChanged()
		{
			NotifyMapTileProviderLocationChanged();
			if (LocationChanged != null)
				LocationChanged(this, EventArgs.Empty);
		}

		/// <summary>
		/// Map provider should be able to know when the location is changed as it may need to update its state, etc.
		/// </summary>
		/// <remarks>
		/// For example, when user changes the map, the proto map tile provider should be able to refresh a list of dates
		/// it has photos for, etc.
		/// </remarks>
		private void NotifyMapTileProviderLocationChanged()
		{
			var mapMode = TileSourceProvider;
			if (mapMode == null) return;

			var currentCentre = Center;
			mapMode.OnLocationChanged(currentCentre);

		}

		private Location _centre = new Location();
		public Location Center
		{
			get { return _centre; }
			set
			{
				if (value == _centre || value == null) return;
				_centre = value;
				OnPropertyChanged(CENTER);
			}
		}

		public static double ZoomLevelFromViewportWidth(double viewportPixelWidth, double logicalViewportWidth)
		{
			var zoom = (viewportPixelWidth / logicalViewportWidth) / 512;
			var zoomLevel = 1 + Math.Log(zoom, 2);
			return zoomLevel;
		}

		public static double ViewportWidthFromZoomLevel(double viewportPixelWidth, double zoomLevel)
		{
			var zoom = Math.Pow(2.0, zoomLevel - 1);
			var viewportWidth = viewportPixelWidth / (zoom * 512);
			return viewportWidth;
		}

		private double _viewportWidth;
		public double ViewportWidth
		{
			get
			{
				return _viewportWidth;
			}
			set
			{
				if (_viewportWidth == value) return;
				_viewportWidth = value;
				OnPropertyChanged(VIEWPORT_WIDTH);
			}
		}

		/// <summary>
		/// Updates the location in one step, firing property changes after all settings have changed.
		/// </summary>
		public void SetLocation(Location center, double viewportWidth)
		{
			if (center == null || (_centre == center && _viewportWidth == viewportWidth)) return;

			if (viewportWidth == 0)
			{
				Center = center;
			}
			else
			{
				_centre = center;
				_viewportWidth = viewportWidth;
				OnPropertyChanged(CENTER, VIEWPORT_WIDTH);
			}
		}

		public void SetLocation(LocationRectangle locationRect)
		{
			//well, now we've got a rectangle we need to display.
			//let's calculate the viewport size
			var newViewportWidth = Math.Max(locationRect.Width/360.0, locationRect.Height/180.0);

			//if even the height is zero, may be it is a dot? Let's zoom to it!
			if (newViewportWidth == 0) newViewportWidth = 0.01;

			SetLocation(locationRect.Center, newViewportWidth);
		}

		#region Mode Properties

		private BaseTileSourceProvider provider;
		public BaseTileSourceProvider TileSourceProvider
		{
			get { return provider; }
			set
			{
				if (value == null) throw new ArgumentNullException("value");
				if (value == provider) return;

				if (provider != null) provider.PropertyChanged -= mode_PropertyChanged;
				provider = value;

				provider.PropertyChanged += mode_PropertyChanged;
				OnPropertyChanged(TILE_SOURCE_PROVIDER);
			}
		}

		private void mode_PropertyChanged(object sender, PropertyChangedEventArgs args)
		{
			if (args.PropertyName == TILE_SOURCE_PROVIDER)
				OnPropertyChanged(TILE_SOURCE_PROVIDER);
			else
				OnPropertyChanged(PROVIDER_TILESOURCES);
		}

		/// <summary>
		/// A list of secondary modes to show, in order, on top of the master mode
		/// </summary>
		public ObservableCollection<BaseTileSourceProvider> SecondaryTileSourceProviders { get; private set; }
		#endregion

		public MapView Clone()
		{
			var clonedModes = new ObservableCollection<BaseTileSourceProvider>(SecondaryTileSourceProviders.Select(m => m.Clone()));

			return new MapView
			{
				Center = _centre,
				ViewportWidth = ViewportWidth,
				TileSourceProvider = TileSourceProvider.Clone(),

				SecondaryTileSourceProviders = clonedModes,

			};
		}

		#region INotifyPropertyChanged
		public event PropertyChangedEventHandler PropertyChanged;

		private void OnPropertyChanged(params string[] propertyNames)
		{
			if (PropertyChanged != null)
				foreach (var propertyName in propertyNames)
					PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
		}

		#endregion

	}
}